-
Notifications
You must be signed in to change notification settings - Fork 65
Reflection and comptime goal #311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Wanted to point out
|
I've added a fix in #312 for the CI failure -- as well as fixed a bug in the template that would still prevent validation without the suggested changes below. |
|
||
Creating new general purpose crates (like serialization crates, log/tracing crates, game engine state inspection crates) that should work with almost all other data structures is nontrivial today. You either need to locally implement your traits for other crates, or the other crates need to depend on you and implement your traits. This often hinders rollout and will never reach everything. | ||
|
||
Reflection offers a way out of this dilemma, as you can write your logic for all types, by processing the type information at runtime (or even preprocess it at compile-time) without requiring trait bounds on your functions or trait impls anywhere. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love the sound of this, but I don't feel like I know exactly what use cases you are trying to enable or aiming to do. Can you give a bit more detail on what kind of things you aim to do? Is the (shiny future) goal to rewrite PartialEq
and friends as some kind of fn
that is written reflectively instead? Would it be generic over a data structure? Is the expectation that we could 'specialize" it to the type (and get efficiency)? Are you not sure yet, but you know the building block is X?
If you could give a high-level sketch of what these APIs might look like, that'd be great, and bonus points if you can sketch the whole set of connections you eventually imagine.
I don't want this goal to have everything designed up front or anything, I just want to understand the big pieces in your head.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I gave some more details, but
Are you not sure yet, but you know the building block is X?
is a very strong motivation here 😆 I have been wanting some sort of const eval that does type stuff for a long time, and consuming types is much easier than producing new types (which I want to do at some point), so why not start there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I second the question. :D I think what's missing is some hint of how you think things will go "out of" const-eval again. Like, I can see const-eval iterate over type information, we an expose almost arbitrary APIs to const-eval if we want to -- but then the typical use of reflection at comptime is to generate code, and const-eval can only generate values. Am I missing something obvious here or how is that supposed to work? Can we feed the result of a const-eval query into the parser and build new HIR from it? I assume that falls under the "insanely hard and requires major refactors" part, but is there a low-cost short-term version of this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The low cost immediately possible version is creating new types and getting them back into the type system with a new type {}
expression that const evaluates to a TypeId.
But I see that as out of scope for this goal. Just getting information out in the form of constants is sufficient for an MVP that would already replace most if not all need for bevy_reflect
and facet
(on nightly, with no RFC or plan for stabilization at this time, just trying to get en par with them to demonstrate feasibility)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just getting information out in the form of constants is sufficient for an MVP that would already replace most if not all need for bevy_reflect and facet
Please go on, how can that be?
Is it that one can then use such const code to build up tables that those crates have their own logic for interpreting at runtime? Hm, I guess that's plausible. I see const-alloc coming up as a feature request very quickly here. ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, they just process the things at runtime or preprocess things in const eval to end up with just table lookups at runtime.
Const alloc is useful but not necessary. Tho I am planning for it the moment we land const traits
|
||
## Design axioms | ||
|
||
* Prefer procedural const-eval code over associated const based designs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there are interesting! I'm having a bit of trouble connecting them to the write-up above, perhaps because I don't quite get what alternatives you are weighing against.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not connect to the write-up above because it is a design axiom for how to write reflection, not why to do reflection. I link to more details below, but TLDR: I do not think uwuflection is the right way forward, as it is a very declarative/functional approach compared to the usual imperative const eval approach we use.
|----------------------|------------------------------------|---------------------------------------------------------------------| | ||
| Lang-team experiment | ![Team][] [lang], [libs] | Needs libstd data structures (lang items) to make the specialization data available | | ||
| Author RFC | | Not at that stage in the next 6 months | | ||
| Lang-team champion | ![Team][] [lang] | TBD | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tmandry would need to assign a champion here
| Design meeting | ![Team][] [lang] | | | ||
| Author call for testing blog post | | Likely will just experiment with bevy or facet, no general call for testing | | ||
|
||
## Frequently asked questions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, some of the details I wanted are here, though I had some trouble understanding some of these questions. e.g., "why not go full zig-style comptime" in particular felt like a bit of a nonsequitor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe because I don't know zig that well
|
||
* Prefer procedural const-eval code over associated const based designs | ||
* We picked `const fn` in general evaluation over associated const based designs that are equally expressive but are essentially a DSL | ||
* Ensure privacy is upheld, modulo things like `size_of` exposing whether new private fields have been added |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How will privacy be enforced? E.g. for serialization, it is important that private fields can be accessed in serde code. At the moment the privacy boundary is natural as the proc-macro generated code is inside the same module as the struct etc. Would it be possible to give serde access to some private fields, but not other random reflection code? Or is the idea that anything exposed to reflection should be the equivalent of a pub struct with pub fields, and any e.g. deserialisation validation would need to be pushed into a later conversion to a different type? Perhaps we'll need some #[reflect]
macro to annotate some fields, though that would open a can of worms of which modifiers from serde and elsewhere to support.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should just punt on this for now and see how far we can get with just public fields. If it turns out you want to be able to create values only via an API that does some sanity checks maybe we'll be able to figure something out, but we'll leave it out of an MVP (I give it a week of the first release of someone sticking data into doc comments on the type that guide their serializer to invoking methods instead of accessing fields 😆 )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Starting with just "everything must be pub
" for the MVP sounds good to me, though it will be useful to keep track of since validation at deserialisation time is a killer feature of serde. Though ... if reflection can inspect (arbitrary) attributes on public fields, then maybe #[reflect(serde::serde(serialize_with = "path"))]
could work where reflect would just provide the outer attribute but not parse anything inside (but e.g. serde could then act based on that)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a question for me too. Public-only makes sense as a starting point but I would like to see some ideas for how it can be extended to private fields too.
Also the ability to talk about whether a type is "complete" and has all fields available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we allow accessing private fields and types, then private fields and types are not a concept that exists anymore. Any change a crate author makes to private fields would become a breaking change now
I can see a system where we allow reflection to find From and Into impls or sth so you can get a public representation of a type with private fields. But I see no world where we'd stabilize private field accesses just like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The devil is in the details of course. We'll end to figure out whether const items in the same module can access private fields even if the access is in a comptime fn defined in another crate. Or whether a comptime fn defined in the same module of a private field can access that private field even if called in a const item outside of it.
I think this experiment is what will give us hands on experience with figuring out which cases are actually a semver issue.
New serialization crates (e.g. for things that are non-goals of `serde` https://github.com/serde-rs/serde/issues/2877) could be used with any types without being limited to types from crates that know about the new serialization crate. | ||
|
||
Furthermore it opens up new possibilities of reflection-like behaviour by | ||
* specializing serialization on specific formats (e.g. serde won't support changing serialization depending on the serializer), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just saying again that we can support serialization features that are non-goals of serde, right? How would it work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess thinking about this specific case again, we can do that today by checking the type id of the serializer in the Serialize impl of a type, as long as that serializer is not generic itself (e.g. over the io::Write)
* specializing serialization on specific formats (e.g. serde won't support changing serialization depending on the serializer), | ||
* specializing trait impl method bodies to have more performant code paths for specific types, groups of types or shapes (e.g. based on the layout) of types. | ||
|
||
I consider reflection orthogonal to derives as they solve similar problems from different directions. Reflection lets you write the logic that processes your types in a way very similar to dynamic languages, by inspecting *values*, while derives generate the code that processes your types ahead of time. Proc macros derives have historically been shown to be fairly hard to debug and bootstrap from scratch. While reflection can get similarly complex fast, it allows for a more dynamic approach where you can easily debug the state your are in, as you do not have to pair the derive logic with the consumer logic (e.g. a serializer) and are instead directly writing just the consumer logic. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a point in between that I thought this proposal was going, where we give compile-time access to static properties of types without a derive. What use cases are enabled by inspecting values as opposed to types? Or are these different framings of the same thing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea the core language feature is about types. The library part of this feature means that we'd want to be able to go from a &T
to a reference to a field of that type
|
||
### The next 6 months | ||
|
||
* add an attribute for `const fn` that prevents them from being called from runtime code or `const fn` without the attribute |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm persuaded this is a need since I've heard others ask for it, but why is it a part of this proposal?
EDIT: Maybe just add a forward-reference to the FAQ here.
| Lang-team experiment | ![Team][] [lang], [libs] | Needs libstd data structures (lang items) to make the specialization data available | | ||
| Lang-team champion | ![Team][] [lang] | TBD | | ||
|
||
### Implement language feature |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this section should be combined with the above.
|
||
## Frequently asked questions | ||
|
||
### Why do you need comptime in addition to reflection? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this better than adding a Reflect
trait with this method (no comptime, no args) or an associated const?
ReflectFromVal
could be added with a &self
method; that could coerce to &dyn ReflectFromVal
.
I guess this is more flexible because it accepts any TypeId, but it only supports calling at compile time which could be limiting, whereas the other approaches seem more forgiving.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what exactly you are proposing, but these operations are inherently comptime-only unless we generate a global reflection static for all types. That said, you can use this scheme to have a Reflect trait that is implemented for all types and has associated consts containing values.
|
||
### The "shiny future" we are working towards | ||
|
||
Create basic building blocks that allow `facet`, `bevy-reflect` and `reflect` to process types without requiring derives or trait bounds. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Facet is relatively new, and seems to have an explicit goal to become a "new ecosystem standard". Do you think it would be worth letting that cook for a bit and waiting for some lessons learned to come out of it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am in contact with reflect people and they want a lot of these things that I'm proposing yesterday 😉
|
||
* Prefer procedural const-eval code over associated const based designs | ||
* We picked `const fn` in general evaluation over associated const based designs that are equally expressive but are essentially a DSL | ||
* Ensure privacy is upheld, modulo things like `size_of` exposing whether new private fields have been added |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a question for me too. Public-only makes sense as a starting point but I would like to see some ideas for how it can be extended to private fields too.
Also the ability to talk about whether a type is "complete" and has all fields available.
Co-authored-by: Nadrieril <[email protected]>
With the renewed general interest in reflection via the
facet
crate coinciding with my own experiments with reflection, and some discussions with bevy folk at the all hands, I think that this may be a good time to pursue more language reflection capabilities thansize_of
andalign_of
.Rendered